/*
   FieldVis - A Tool for Visualizing Astrophysical Magnetohydrodynamic Flow
   
   Copyright (C) 2006 - 2008     FieldVis Development Team
  
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.
  
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
  
   You should have received a copy of the GNU General Public License along
   with this program; if not, write to the Free Software Foundation, Inc.,
   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
   File     : FieldVis.cpp
   Author   : Blayne Field
   
   Purpose  : The main driver file for the FieldVis program
*/

#include <GL/glut.h>

#include <iostream>
#include <cstdio>
#include <string>

#include "UIState.h"
#include "Streamtube.h"
#include "ConfigFile.h"
#include "flowviewer.h"
#include "capture.h"

// external function prototypes
extern void ParseExternalRCFile(std::vector<std::string> &rc);

// internal function prototypes (found after main)
void PrintGridLocation();
void processHits(GLint hits, GLuint *buffer);
void pickRendering(int X, int Y);
void LookDownAnAxis(Vector3D &look, Vector3D &up);

// internal OpenGL function callback prototypes
void OnDisplay(void);
void OnReshape(int Width, int Height);
void OnMouseButton(int Button, int State, int X, int Y);
void OnMouseMotion(int X, int Y);
void OnPassiveMouseMotion(int X, int Y);
void Animate();
void OnKey(unsigned char Key, int X, int Y);


// the global variables
// the current state of the OpenGL user interface
UIState  ui(GLUT_BITMAP_TIMES_ROMAN_10);

// the container holding our configuration files and by extension our streamtubes
ConfigFileManager  cfm;


int main(int argc, char *argv[])
{
   GLfloat mat_ambient[] = { 0.1, 0.1, 0.1, 1.0};
   GLfloat mat_diffuse[] = { 0.6, 0.6, 0.6, 1.0};
   GLfloat mat_specular[] = {0.0, 0.0, 0.0, 1.0};

   GLfloat light_position[] = {1.0, 1.0, 1.0, 0.0};
   GLfloat white_light[] = {1.0, 1.0, 1.0, 1.0};
   GLfloat lmodel_ambient[] = {0.1, 0.1, 0.1, 1.0};
   //GLfloat diffuseMaterial[] = {.5, .5, .5, 1.0};

   if (argc < 2)
   {
      std::cerr << "Usage: fieldvis config_file [config_file...]" << std::endl;
      exit(1);
   }
   
   // The resource container.
   std::vector<std::string>   RCPaths;
   
   ParseExternalRCFile(RCPaths);

   // initialize GLUT
   glutInit(&argc, argv);
   glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);

   // setup rendering window
   ui.wa = new WindowAttributes(DEFAULT_WIN_WIDTH, DEFAULT_WIN_HEIGHT); 
   glutInitWindowSize(ui.wa->Width(), ui.wa->Height());
   glutInitWindowPosition(0, 0);
   ui.wa->ID(glutCreateWindow("FieldVis")); 
   glViewport(0, 0, ui.wa->Width(), ui.wa->Height());
   
   // set up shading model
   glShadeModel(GL_SMOOTH);

   // set up streamtube material properties
   glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
   glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
   glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
   glMaterialf(GL_FRONT, GL_SHININESS, 100.0);
   
   // enable the color material properties
   glEnable(GL_COLOR_MATERIAL);

   // set up light properties
   glLightfv(GL_LIGHT0, GL_POSITION, light_position);
   glLightfv(GL_LIGHT0, GL_DIFFUSE, white_light); 
   glLightfv(GL_LIGHT0, GL_SPECULAR, white_light);
   glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);

   // enable lighting
   glEnable(GL_LIGHT0);
   glEnable(GL_LIGHTING);
   
   // enable blending
   glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
   glEnable(GL_BLEND);

   // load the config files  
   for (int i = 1; i < argc; i++)
   {
      cfm.Manage( new ConfigFile(argv[i], RCPaths) );
   }

   // get the size of the bounding box
   float xmax, ymax, zmax;
   
   cfm[0]->SetBoxSize(&xmax, &ymax, &zmax);
   xmax *= BB_X_MAX;
   ymax *= BB_Y_MAX;
   zmax *= BB_Z_MAX;

   // finish setting up the UI for the bounding box and selection plane
   ui.SetUpBBoxAndPlane(xmax, ymax, zmax);

   // setup GLUT callback functions
   glutDisplayFunc(OnDisplay);
   glutReshapeFunc(OnReshape);
   glutIdleFunc(NULL);            // NULL here but replaced with Animate() during animation.
   glutMouseFunc(OnMouseButton);
   glutMotionFunc(OnMouseMotion);
   glutPassiveMotionFunc(OnPassiveMouseMotion);
   glutKeyboardFunc(OnKey);

   // begin GLUT event loop
   glutMainLoop();
}

// internally defined functions

void PrintGridLocation()
{
   char buffer[64];
   Point location = ui.cubeLocation * cfm[0]->GetScaleFactor();
   
   sprintf(buffer, "[x: %0.4f, y: %0.4f, z: %0.4f]",
           location.GetX(),
           location.GetY(),
           location.GetZ());

   std::string output(buffer);
   
   glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
   glPushAttrib(GL_ENABLE_BIT);
   glDisable(GL_TEXTURE_2D);
   ui.writer->DrawText(5, 10 + cfm.Count() * (5 + ui.wa->Height() / 50), output, LEFT_JUST);
   glPopAttrib();
}


void processHits(GLint hits, GLuint *buffer)
{
   if (hits > 0)
   {
      // search for the min in buffer[1] to find the 'topmost' hit
      int hitIndex = 0;
      GLuint min = buffer[1];
      for (int i = 0; i < hits; i++)
      {
         // for whatever reason a straight unsigned comparison like the following
         // if ( min > buffer[ i*6 + 1 ] ) doesn't give the right result - so to get
         // the true min we actually need the max!
         if ( min > buffer[ i * 6 + 1 ] )
         {
            hitIndex = i;
            min = buffer[ i * 6 + 1 ];
         }
      }
      
      ui.configIndex = static_cast<int>(buffer[hitIndex * 6 + 3]);
      ui.tubeIndex   = static_cast<int>(buffer[hitIndex * 6 + 4]);
      ui.pointIndex  = static_cast<int>(buffer[hitIndex * 6 + 5]);

      ui.cubeLocation = cfm[ui.configIndex]->GetStreamlinePoint(ui.tubeIndex, ui.pointIndex);
      
      if (ui.plane->GetPlaneState() == SelectionPlane::NONE)
      {
         ui.plane->SetPlaneState(SelectionPlane::YZ);
      }      
      ui.movingStreamline = true;
   }
}


void pickRendering(int X, int Y)
{
   const int BUFSIZE = 512;
   GLuint selectBuf[BUFSIZE];         
   GLint hits, viewport[4];
         
   glGetIntegerv(GL_VIEWPORT, viewport);
   glSelectBuffer(BUFSIZE, selectBuf);
   glRenderMode(GL_SELECT);
   glInitNames();
   glMatrixMode(GL_PROJECTION);
   glPushMatrix();
   glLoadIdentity();
   gluPickMatrix((GLdouble) X, (GLdouble) (viewport[3] - Y), 5.0, 5.0, viewport);
   gluPerspective(FOV_ANGLE, ui.wa->AspectRatio(), 0.01, 10.0);

   // We don't need to render the complete faces here - just the points            
   for (unsigned int index = 0; index < cfm.Count(); index++)
   {
      glPushName(index);
      cfm[index]->DrawStreamtubes(GL_POINT);
      glPopName();
   }

   glMatrixMode(GL_PROJECTION);
   glPopMatrix();
   glFlush();

   hits = glRenderMode(GL_RENDER);
   processHits(hits, selectBuf);
}


// look and up vectors must be normalized prior to calling this function.
// make sure the side vector is perpendicular
void LookDownAnAxis(Vector3D &look, Vector3D &up)
{
   Vector3D Side( look.Cross(up) );
   Vector3D Pos( ui.origin - (look * VIEW_START_DIST_FROM_ORIGIN) );

   ui.camera.SetState(Pos, look, up, Side);
}

// OpenGL callback functions

void OnDisplay(void)
{
   // clear window (make sure stencil buffer writing is enabled only long
   //   enough to clear the buffer)
   glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

   // enable smooth lines
   glEnable(GL_LINE_SMOOTH);

   // set hints for highest quality rendering
   glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
   glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
   glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

   // set polygon offset
   glEnable(GL_POLYGON_OFFSET_FILL);
   glPolygonOffset(1.0, 1.0);
   
   glDisable(GL_DEPTH_TEST);

   // set projection
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   gluOrtho2D(0, ui.wa->Width(), 0, ui.wa->Height());

   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();

   // draw the colormaps if enabled
   int height = ui.wa->Height() / 50;
   if (ui.drawColorMaps)
   {
      for (unsigned int j = 0; j < cfm.Count(); j++)
      {
         cfm[j]->GetColorMap()->DrawColorMap(5, 5 + (3 * j * height) / 2,
                                             ui.wa->Width() / 3, height, ui.writer);
      }
   }
   
   // if selection plane is showing - print the coordinates above the colormaps
   if (ui.plane->GetPlaneState() != SelectionPlane::NONE)
   {
      PrintGridLocation();
   }

   glEnable(GL_DEPTH_TEST);
   
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();

   if (ui.orthographic)
   {
      glOrtho(-ui.wa->AspectRatio(), ui.wa->AspectRatio(), -1, 1, 0.01, 10);
   }
   else
   {
      gluPerspective(FOV_ANGLE, ui.wa->AspectRatio(), 0.01, 10.0);
   }

   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   
   // set view position
   gluLookAt(ui.camera.Pos.GetX(), ui.camera.Pos.GetY(), ui.camera.Pos.GetZ(),
             ui.camera.Look.GetX() + ui.camera.Pos.GetX(),
             ui.camera.Look.GetY() + ui.camera.Pos.GetY(),
             ui.camera.Look.GetZ() + ui.camera.Pos.GetZ(),
             ui.camera.Up.GetX(), ui.camera.Up.GetY(), ui.camera.Up.GetZ());

   // draw the bounding box
   ui.bBox->Draw();

   // draw the streamtubes
   for (unsigned int j = 0; j < cfm.Count(); j++)
   {
      cfm[j]->DrawStreamtubes(ui.displayMode.GetDisplayMode());
   }

   // draw the selection cube - we want it unlit
   glDisable(GL_LIGHT0);
   if (ui.plane->GetPlaneState() != SelectionPlane::NONE)
   {
      ui.cube.Draw(ui.cubeLocation);
   }
   glEnable(GL_LIGHT0);

   // draw the selection plane at the point provided
   ui.plane->Draw(ui.cubeLocation);

   glFlush();
   glutSwapBuffers();
}


void OnReshape(int Width, int Height)
{
   // store new width and height
   ui.wa->Width(Width);
   ui.wa->Height(Height);

   // resize window
   glutReshapeWindow(ui.wa->Width(), ui.wa->Height());
   glViewport(0, 0, ui.wa->Width(), ui.wa->Height());
}


void OnMouseButton(int Button, int State, int X, int Y)
{
   ui.ShiftDown = glutGetModifiers() & GLUT_ACTIVE_SHIFT != 0;
   
   if (Button == GLUT_LEFT_BUTTON)
   {
      // store button state
      ui.MouseLastX = X;
      ui.MouseLastY = Y;
      ui.LeftBtnDown = (State == GLUT_DOWN);

      // start view spinning by storing current mouse coordinates and camera
      // state, this way we can always determine the camera orientation from
      // the original orientation plus displacement, making it easy to return
      // to the initial position
      if (State == GLUT_DOWN)
      {
         ui.LeftBtnDownX = X;
         ui.LeftBtnDownY = Y;
         ui.camera.PushState();
      }

      if (State == GLUT_DOWN)
      {
         // render for selection
         pickRendering(X, Y);
      }
   }
   else if (Button == GLUT_MIDDLE_BUTTON)
   {
      ui.MidBtnDown = (State == GLUT_DOWN);
   }
   else if (Button == GLUT_RIGHT_BUTTON)
   {
      ui.RightBtnDown = (State == GLUT_DOWN);
   }

   glutPostRedisplay();
}


void OnMouseMotion(int X, int Y)
{
   // handle mouse movement with one or more mouse buttons pressed
   if (ui.LeftBtnDown)
   {
      ui.camera.PopState();
      ui.camera.Orbit(ui.origin, X - ui.LeftBtnDownX, ui.LeftBtnDownY - Y);  
   }
   else if (ui.MidBtnDown)
   {
      // determine speed adjustment factor based on texture zoom
      double ZoomFactor = (ui.TexZoomed ? MOUSE_ZOOMED_TEX_SPEED_FACTOR * CAMERA_MOUSE_DIST
                                       : CAMERA_MOUSE_DIST);

      if (ui.ShiftDown)
      {
         ui.camera.Zoom((Y - ui.MouseLastY) * ZoomFactor);
      }
      else
      {
         ui.camera.Pan((ui.MouseLastX - X) * ZoomFactor, (Y - ui.MouseLastY) * ZoomFactor);
      }
   }
   else if (ui.RightBtnDown)
   {
      ui.camera.Roll((ui.MouseLastX - X) * CAMERA_MOUSE_DIST);
   }

   // store new mouse coordinates
   ui.MouseLastX = X;
   ui.MouseLastY = Y;

   glutPostRedisplay();
}


void OnPassiveMouseMotion(int X, int Y)
{
   // store new mouse coordinates
   ui.MouseLastX = X;
   ui.MouseLastY = Y;
}

// This function is responsible for the animation capabilities - unfortunately
// it forces the CPU utilization to 100% whenever it is loaded, so we only load
// when performing captures and unload immediately upon finishing.
// It is loaded into OpenGLs idle function processing using glutIdleFunc()
void Animate()
{
   if (ui.animation.getState() == Animation::ACTIVE
       && ui.animation.getFrameNumber() < ui.animation.getTotalFrames())
   {
      Vector3D pivot(0.5, 0.5, 0.5);
      ui.camera.Orbit(pivot, 1, 0, ui.animation.getTotalFrames() / 2);
      glutPostRedisplay();  
      
      std::string file = ui.animation.GetFilename();
      glDumpWindowPPM(file.c_str(), ui.wa->Width(), ui.wa->Height());  
   }

   if (ui.animation.getState() == Animation::ACTIVE
       && ui.animation.getFrameNumber() == ui.animation.getTotalFrames())
   {
      ui.animation.setState(Animation::INACTIVE);
      glutIdleFunc(NULL);  // unload the animation module
      ui.animation.resetFrameNumber();
   }
}



void OnKey(unsigned char Key, int X, int Y)
{
   const float courseMovement = 0.01;
   const float fineMovement = 0.001;
   
   bool doRedisplay = true;

   Vector3D Look, Up, Side, Pos;
   
   static int x_view = -1;
   static int y_view = -1;
   static int z_view = -1;
   
   float movement = courseMovement;

   switch (Key)
   {
      // quit application
      case 27: // Esc
         exit(0);
         break;

         // toggle bounding box display
      case 'b':
         ui.bBox->ToggleDrawState();
         break;

         // toggle color map display
      case 'c':
         ui.drawColorMaps = ! ui.drawColorMaps;
         break;

         // toggle the display mode (mesh, solid, or vertices)
      case 'f':
         ui.displayMode.CycleDisplayMode();
         break;

      case '7':
         if (ui.movingStreamline && ui.pointIndex > 0)
         {
            ui.pointIndex--;
            ui.cubeLocation = cfm[ui.configIndex]->GetStreamlinePoint(ui.tubeIndex, ui.pointIndex);
         }
         break;

      case '8':
         if (ui.movingStreamline 
             && ui.pointIndex < cfm[ui.configIndex]->GetStreamtubeLength(ui.tubeIndex) - 1)
         {
            ui.pointIndex++;
            ui.cubeLocation = cfm[ui.configIndex]->GetStreamlinePoint(ui.tubeIndex, ui.pointIndex);
         }
         break;

         // move the current plane in the negative direction
      case '9':
         movement = fineMovement;
         // falls through
      case '-':
         if (ui.plane->GetPlaneState() == SelectionPlane::YZ)
            if ((ui.cubeLocation.GetX() - movement) > 0)
               ui.cubeLocation.MoveX(-movement);
            else
               ui.cubeLocation.SetX(0);
         else if (ui.plane->GetPlaneState() == SelectionPlane::XZ)
            if ((ui.cubeLocation.GetY() - movement) > 0)
               ui.cubeLocation.MoveY(-movement);
            else
               ui.cubeLocation.SetY(0);
         else if (ui.plane->GetPlaneState() == SelectionPlane::XY)
            if ((ui.cubeLocation.GetZ() - movement) > 0)
               ui.cubeLocation.MoveZ(-movement);
            else
               ui.cubeLocation.SetZ(0);

         if (ui.movingStreamline)
            ui.pointIndex = cfm[ui.configIndex]->MoveStreamlineSeed(ui.tubeIndex, ui.cubeLocation);
         
         break;

         // move the current plane in the positive direction
      case '0':
         movement = fineMovement;
         // falls through
      case '=':
         if (ui.plane->GetPlaneState() == SelectionPlane::YZ
             && (ui.cubeLocation.GetX() + movement) < ui.bBox->X())
            ui.cubeLocation.MoveX(movement);
         else if (ui.plane->GetPlaneState() == SelectionPlane::XZ
                  && (ui.cubeLocation.GetY() + movement) < ui.bBox->Y())
            ui.cubeLocation.MoveY(movement);
         else if (ui.plane->GetPlaneState() == SelectionPlane::XY
                  && (ui.cubeLocation.GetZ() + movement) < ui.bBox->Z())
            ui.cubeLocation.MoveZ(movement);

         if (ui.movingStreamline)
            ui.pointIndex = cfm[ui.configIndex]->MoveStreamlineSeed(ui.tubeIndex, ui.cubeLocation);
         
         break;

      case 'i':    
         cfm[ui.configIndex]->AddStreamlineSeed(ui.cubeLocation);
         break;

      case 'd':
         if (ui.movingStreamline)
         {
            cfm[ui.configIndex]->RemoveStreamlineSeed(ui.tubeIndex);
         }
         ui.movingStreamline = false;
         break;

         // cycle through the current plane orientation
      case 'p':
         ui.plane->CycleDrawState();
         if (ui.plane->GetPlaneState() == SelectionPlane::NONE)
         {
            ui.movingStreamline = false;
         }
         break;

         // re-read the config files
      case 'r':
         for (unsigned int i = 0; i < cfm.Count(); i++)
         {
            cfm[i]->Reload();
         }
         break;

         // save the seed points
      case 's':
         for (unsigned int i = 0; i < cfm.Count(); i++)
         {
            cfm[i]->WriteSeedsToFile();
         }
         break;

         // start the rotation of the cube
      case 'a':
         if (ui.animation.getState() == Animation::REQUESTED)
         {
            ui.animation.setState(Animation::ACTIVE);
            glutIdleFunc(Animate);     // Load the animation module
         }
         else if (ui.animation.getState() == Animation::INACTIVE)
         {
            int count;
            std::string basename;
            
            ui.animation.setState(Animation::REQUESTED);

            std::cout << "Input file name (numbers will be appended): ";
            std::cin >> basename;
            std::cout << "How many frames? ";
            std::cin >> count;

            ui.animation.SetUp(basename, count);

            std::cout << "Press 'a' in the rendering window to begin animation." << std::endl;
         }
         break;
         
         // toggle orthographic display
      case 'o':
         ui.orthographic = ! ui.orthographic;
         break;

         // look down the x-axis
      case 'x':        
         x_view = -x_view;
         // set the initial camera orientation
         Look = Vector3D(x_view, 0.0, 0.0);
         Up = Vector3D(0.0, 1.0, 0.0);
         LookDownAnAxis(Look, Up);
         break;

         // look down the y-axis
      case 'y':   
         y_view = -y_view;
         // set the initial camera orientation
         Look = Vector3D(0.0, y_view, 0.0);
         Up = Vector3D(0.0, 0.0, 1.0);
         LookDownAnAxis(Look, Up);
         break;

         // look down the z-axis
      case 'z':  
         z_view = -z_view;
         // set the initial camera orientation
         Look = Vector3D(0.0, 0.0, z_view);
         Up = Vector3D(1.0, 0.0, 0.0);
         LookDownAnAxis(Look, Up);
         break;

         // take a screenshot
      case 'w':
         {  // need the enclosing scope here because the compiler complains about 
            // case crossing initialization.
            std::string filename;
            std::cout << "Input file name (no extension):" << std::endl;
            std::cin >> filename;
            filename += ".ppm";

            glDumpWindowPPM(filename.c_str(), ui.wa->Width(), ui.wa->Height());
         }
         // fall through
         
         // prevent redisplay
      default:
         doRedisplay = false;
         return;
   }
   if (doRedisplay)
   {
      glutPostRedisplay();
   }
}

